#include "TextureCubeProg.h"
#include "camera.h"
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLFunctions_3_3_Core>
#include <QImage>
#include <QOpenGLTexture>


static GLfloat  vertices[] = {
    // positions            // vertex norms         // texture coords
     0.5f,  0.5f,  0.5f,    0.0f,  0.0f,  1.0f,     1.0f, 1.0f,     // front
    -0.5f,  0.5f,  0.5f,    0.0f,  0.0f,  1.0f,     0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,    0.0f,  0.0f,  1.0f,     0.0f, 0.0f,
    -0.5f, -0.5f,  0.5f,    0.0f,  0.0f,  1.0f,     0.0f, 0.0f,
     0.5f, -0.5f,  0.5f,    0.0f,  0.0f,  1.0f,     1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,    0.0f,  0.0f,  1.0f,     1.0f, 1.0f,

     0.5f, -0.5f, -0.5f,    0.0f,  0.0f, -1.0f,     0.0f, 0.0f,     // back
    -0.5f, -0.5f, -0.5f,    0.0f,  0.0f, -1.0f,     1.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,    0.0f,  0.0f, -1.0f,     1.0f, 1.0f,
    -0.5f,  0.5f, -0.5f,    0.0f,  0.0f, -1.0f,     1.0f, 1.0f,
     0.5f,  0.5f, -0.5f,    0.0f,  0.0f, -1.0f,     0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,    0.0f,  0.0f, -1.0f,     0.0f, 0.0f,

     0.5f,  0.5f,  0.5f,    0.0f,  1.0f,  0.0f,     1.0f, 0.0f,     // top
     0.5f,  0.5f, -0.5f,    0.0f,  1.0f,  0.0f,     1.0f, 1.0f,
    -0.5f,  0.5f, -0.5f,    0.0f,  1.0f,  0.0f,     0.0f, 1.0f,
    -0.5f,  0.5f, -0.5f,    0.0f,  1.0f,  0.0f,     0.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,    0.0f,  1.0f,  0.0f,     0.0f, 0.0f,
     0.5f,  0.5f,  0.5f,    0.0f,  1.0f,  0.0f,     1.0f, 0.0f,

     0.5f, -0.5f,  0.5f,    0.0f, -1.0f,  0.0f,     1.0f, 1.0f,     // buttom
     -0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,     0.0f, 1.0f,
     -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,     0.0f, 0.0f,
     -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,     0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,    0.0f, -1.0f,  0.0f,     1.0f, 0.0f,
     0.5f, -0.5f,  0.5f,    0.0f, -1.0f,  0.0f,     1.0f, 1.0f,

     -0.5f,  0.5f,  0.5f,   -1.0f,  0.0f,  0.0f,     1.0f, 1.0f,     // left
     -0.5f,  0.5f, -0.5f,   -1.0f,  0.0f,  0.0f,     0.0f, 1.0f,
     -0.5f, -0.5f, -0.5f,   -1.0f,  0.0f,  0.0f,     0.0f, 0.0f,
     -0.5f, -0.5f, -0.5f,   -1.0f,  0.0f,  0.0f,     0.0f, 0.0f,
     -0.5f, -0.5f,  0.5f,   -1.0f,  0.0f,  0.0f,     1.0f, 0.0f,
     -0.5f,  0.5f,  0.5f,   -1.0f,  0.0f,  0.0f,     1.0f, 1.0f,

     0.5f,  0.5f,  0.5f,    1.0f,  0.0f,  0.0f,     0.0f, 1.0f,     // right
     0.5f, -0.5f,  0.5f,    1.0f,  0.0f,  0.0f,     0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,    1.0f,  0.0f,  0.0f,     1.0f, 0.0f,
     0.5f, -0.5f, -0.5f,    1.0f,  0.0f,  0.0f,     1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,    1.0f,  0.0f,  0.0f,     1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,    1.0f,  0.0f,  0.0f,     0.0f, 1.0f
};


TextureCubeProg::TextureCubeProg(QObject *parent)
    : TextureProg(parent)
    , MY_GL_FUNC(nullptr)
    , program(nullptr)
    , vao(nullptr)
    , vbo(nullptr)
    , diffuseMap(nullptr)
    , specularMap(nullptr)
    , modelLoc(-1)
    , viewLoc(-1)
    , projectionLoc(-1)
    , viewPosLoc(-1)
{
}

TextureCubeProg::~TextureCubeProg()
{
    teardownGL();
}

void TextureCubeProg::initializeGL(QOpenGLContext *ctx, void* arg, size_t argLen)
{
    TextureProg::initializeGL(ctx, arg, argLen);
    MY_GL_FUNC = ctx->versionFunctions<QOpenGLFunctions_3_3_Core>();

    // create vertex buffer objects (VBO)
    vbo = new QOpenGLBuffer;
    bool rc = vbo->create();
    Q_ASSERT(rc);

    rc = vbo->bind();
    Q_ASSERT(rc);

    //vbo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    qDebug() << Q_FUNC_INFO << "VBO usage pattern" << vbo->usagePattern();
    vbo->allocate(vertices, sizeof(vertices));

    // create VAO
    rc = false;
    vao = new QOpenGLVertexArrayObject(this);
    rc = vao->create();
    Q_ASSERT(rc);

    vao->bind();

    // initialize 'material_'
    material_.diffuse = QImage(":/texture/diffuse_maps.png");
    Q_ASSERT(! material_.diffuse.isNull());
    material_.specular = QImage(":/texture/specular_maps.png");
    Q_ASSERT(! material_.specular.isNull());
    material_.shininess = 32.0f;

    // load maps
    diffuseMap = new QOpenGLTexture(material_.diffuse.mirrored());
    diffuseMap->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
    diffuseMap->setMagnificationFilter(QOpenGLTexture::Linear);
    specularMap = new QOpenGLTexture(material_.specular.mirrored());
    specularMap->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
    specularMap->setMagnificationFilter(QOpenGLTexture::Linear);

    // create program
    program = new QOpenGLShaderProgram(this);

    rc = program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/texture_cube.vert");
    Q_ASSERT(rc);

    rc = program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/texture_cube.frag");
    Q_ASSERT(rc);

    rc = program->link();
    Q_ASSERT(rc);

    rc = program->bind();
    Q_ASSERT(rc);

    program->enableAttributeArray(0);
    program->enableAttributeArray(1);
    program->enableAttributeArray(2);
    program->setAttributeBuffer(0, GL_FLOAT, 0, 3, 8 * sizeof(GLfloat));
    program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(GLfloat), 3, 8 * sizeof(GLfloat));
    program->setAttributeBuffer(2, GL_FLOAT, 6 * sizeof(GLfloat), 2, 8 * sizeof(GLfloat));

    // Cache Uniform Locations
    modelLoc = program->uniformLocation("model");
    Q_ASSERT(modelLoc >= 0);

    viewLoc = program->uniformLocation("view");
    Q_ASSERT(viewLoc >= 0);

    projectionLoc = program->uniformLocation("projection");
    Q_ASSERT(projectionLoc >= 0);

    viewPosLoc = program->uniformLocation("viewPos");
    Q_ASSERT(viewPosLoc >= 0);

    // assign texture units
    program->setUniformValue("material.diffuse", 0);
    program->setUniformValue("material.specular", 1);

    // Release (unbind) all
    program->release();
    diffuseMap->release();
    specularMap->release();
    vao->release();
    vbo->release();
}

void TextureCubeProg::paintGL(const Camera &camera, const QMatrix4x4& projection, const Light &light)
{
    bool rc = false;

    rc = program->bind();
    Q_ASSERT(rc);

    diffuseMap->bind(0);
    specularMap->bind(1);

    program->setUniformValue(modelLoc, toMatrix());
    program->setUniformValue(viewLoc, camera.viewMatrix());
    program->setUniformValue(projectionLoc, projection);
    program->setUniformValue(viewPosLoc, camera.position);
    program->setUniformValue("normal", toMatrix().normalMatrix());

    program->setUniformValue("material.shininess", material_.shininess);

    program->setUniformValue("light.position", light.position);
    program->setUniformValue("light.ambient", light.ambient);
    program->setUniformValue("light.diffuse", light.diffuse);
    program->setUniformValue("light.specular", light.specular);

    vao->bind();

    MY_GL_FUNC->glDrawArrays(GL_TRIANGLES, 0, sizeof(vertices) / (8*sizeof(GLfloat)));

    vao->release();
    specularMap->release();
    diffuseMap->release();
    program->release();
}

void TextureCubeProg::teardownGL()
{

    if (diffuseMap) {
        delete diffuseMap;
        diffuseMap = nullptr;
    }

    if (specularMap) {
        delete specularMap;
        specularMap = nullptr;
    }

    if (vao) {
        delete vao;
        vao = nullptr;
    }

    if (vbo) {
        delete vbo;
        vbo = nullptr;
    }

    if (program) {
        delete program;
        program = nullptr;
    }

    modelLoc = viewLoc = projectionLoc = viewPosLoc = -1;
    MY_GL_FUNC = nullptr;

    TextureProg::teardownGL();
}
